#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math

if __name__ == '__main__':
    import direct.directbase.DirectStart

import pygame
from direct.task import Task
from direct.actor import Actor
from direct.showbase import DirectObject
from pandac.PandaModules import *
from direct.interval.IntervalGlobal import *

import gui
import parse
import particle

from utils import *

class Level(DirectObject.DirectObject):
    def __init__(self, name, options=None, difficulty="Normal", joystick=None, camera=base.camera):
        ## Constantes
        self.FLY_AREA_W = 14.0
        self.FLY_AREA_H = 12.0
        
        self.FLY_AREA_R = self.FLY_AREA_W/2
        self.FLY_AREA_L = -self.FLY_AREA_W/2
        self.FLY_AREA_T = self.FLY_AREA_H/2
        self.FLY_AREA_B = -self.FLY_AREA_H/2
        
        self.RING_SPACING_PER_BEAT = 20
        
        self.SPEED_SCALE = .4
        
        self.CONTROL_UPDATE_DELAY = 1.0/60.0
    
        self.rootNode = render.attachNewNode("Level Root Node")
        
        self.miss_sound = loader.loadSfx('./sound/miss.wav')
        
        self.options = options
        
        self.name = name
        self.difficulty = difficulty
        
        self.y_signal = -1
        if self.options and self.options.get('game-opts', 'inverse-y-axis') == 'yes':
                self.y_signal = 1
    
    def setup(self):
        self.setup_logic()
        self.setup_graphics()
        self.setup_gui()
        self.setup_rings()
        
        #self.setup_controller(joystick)
        self.setup_events()
    
    def setup_logic(self):
        self.info = parse.level_header(self.name)
        
        #################
        ## Música
        print self.info["MUSIC_FILE"]
        
        #self.effect = base.sfxManagerList[0].createDsp(base.sfxManagerList[0].DSPDistortion)
        
        self.music = loader.loadMusic(self.info["MUSIC_FILE"])
        self.music_bpm = self.info["BPM"]
        
        self.BEAT_DELAY = beat_delay(self.music_bpm)
        
        #self.music_length = (self.music.length()/60.0)*self.music_bpm * bpm2msdelay(self.music_bpm)
        #self.length = self.music_length*60
        #print "music length", self.music_length

        #################
        ## Pontuação, Chain e Vida
        
        self.score = 0
        self.chain = 0
        self.max_chain = 0
        
        self.MAX_LIFE = 20
        self.LIFE_FILL_THRESHOLD = 4
        
        self.life = 7

        #################
        ## Dicionários
        self.score_map = {
            "PERFECT" : 200,
            "GOOD" : 100,
            "OK" : 50,
            "BAD" : 5,
            "MISS" : 0,
        }
        
        self.precision_judge = {
            0.03 : "PERFECT",
            0.06 : "GOOD",
            0.1 : "OK",
            0.2 : "BAD",
            1.0 : "MISS",
        }
        
        self.score_list = self.precision_judge.keys()[:]
        self.score_list.sort()
        
        self.judgement_stats = {
            "PERFECT" : 0,
            "GOOD" : 0,
            "OK" : 0,
            "BAD" : 0,
            "MISS" : 0,
        }

    def setup_gui(self):
        #################
        ## Decorações de Tela
        self.deco_mgr = gui.ScreenDecorationManager()
        self.btn_viewer = gui.ButtonViewer(self.music_bpm,z_pos=0.7)
        #self.life_bar = gui.LifeBar()
        self.score_display = gui.ScoreDisplay()

    def setup_graphics(self):
        #################
        ## Ator principal
        self.bunnyActor = Actor.Actor("models/bunny_boy", {
                "fly": "models/bunny_boy-fly",
                "turn-left": "models/bunny_boy-turn-left",
                "turn-right": "models/bunny_boy-turn-right",
                "dive": "models/bunny_boy-dive",
                "rise": "models/bunny_boy-rise",
        })
        self.bunnyActor.setScale(0.1, 0.1, 0.1)
        self.bunnyActor.setHpr(180, 0, 0)
        self.bunnyActor.setPos(.0, .0, .0)
        self.bunnyActor.reparentTo(self.rootNode)
        self.bunnyActor.loop("fly")
        self.bunnyActor.setLightOff()
        self.bunnyActor.speed = Vec3(0, 0, 0)
        self.bunnyActor.last_update = 0
        
        #~ self.p = particle.BunnyParticles()
        #~ self.p.start(render)
        #~ p_node = self.p.getParticlesNamed('particles-1').getNode()
        #~ p_node.reparentTo(self.bunnyActor)
        
        #################
        ## Inicialização de camera
        self.camera = camera
        
        self.camera.setPos(0, -3, 0.3)
        self.camera.lookAt(self.bunnyActor)
        self.camera_z_offset = 0.4
        self.camera.setZ(0.4)
        self.camera_offset = 3.5

        base.cam.node().getLens().setFar(5000)

        #################
        ## self.skybox
        self.skybox = loader.loadModel("./models/skybox")
        #self.skybox.setH(-30)
        
        self.skybox.setZ(-15)
        
        self.skybox.reparentTo(self.rootNode)
        self.skybox.setFogOff()
        self.skybox.setLightOff()
        
        self.skybox.setScale(80)

        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4( 2.0, 2.0, 2.0, 1 ))
        self.skybox.attachNewNode(ambientLight) 
        
        interval_hpr_skybox = LerpHprInterval(self.skybox, duration=1000.0, startHpr=VBase3(0, 0, 0), hpr=VBase3(360, 0, 0))
        interval_hpr_skybox.loop()
        
        #################
        ## Terreno
        self.TERRAIN_Z = -15
        self.TERRAIN_PATCHES = 20
        self.TERRAIN_PATCHES_W = 1
        
        self.terrain_patch_size = 39.9934616089 
        
        #print "terrain-size:", self.terrain_patch_size
        
        self.terrain_patch_list = []
        
        for i in range(self.TERRAIN_PATCHES):
            terrain_index = (i % 8) + 1
            
            #print "./models/terrain_%d" % terrain_index
            
            terrain = loader.loadModelCopy("./models/terrain_%d" % terrain_index)
            
            
            #~ color = [VBase4(0,0,1,0),
            #~ VBase4(0,1,0,0),
            #~ VBase4(1,0,0,0),
            #~ VBase4(0,.5,.5,0),
            #~ VBase4(.5,0,.5,0),
            #~ VBase4(.5,.5,0,0),
            #~ VBase4(.25,.25,.75,0),
            #~ VBase4(1.,.6,.3,0),][terrain_index-1]
            
            #~ terrain.setColor(color)
            
            terrain.setPos(.0, 24 + self.terrain_patch_size*i - 0.1, self.TERRAIN_Z)
            terrain.reparentTo(self.rootNode)
            
            self.terrain_patch_list.append(terrain)

        
        #################
        ## Fog
        fog = Fog('distanceFog')
        fog.setColor(Vec4(0.2490196078431372549, 0.80392156862745098039, 0.97254901960784313725, 1))
        fog.setExpDensity(.002)
        render.setFog(fog)

        #################
        ## Iluminação
        # Create Ambient Light
        ambientLight = AmbientLight( 'ambientLight' )
        ambientLight.setColor( Vec4( 0.4, 0.4, 0.4, 1 ) )
        ambientLightNP = render.attachNewNode( ambientLight )
        self.rootNode.setLight(ambientLightNP)

        # Directional light 01
        directionalLight = DirectionalLight( "directionalLight" )
        directionalLight.setColor( Vec4( 1, 1, 1, 1 ) )
        directionalLightNP = self.rootNode.attachNewNode( directionalLight )
        # This light is facing backwards, towards the camera.
        directionalLightNP.setHpr(180, -20, 0)
        self.rootNode.setLight(directionalLightNP)

        # Directional light 02
        directionalLight = DirectionalLight( "directionalLight" )
        directionalLight.setColor( Vec4( 0.6, 0.6, 0.6, 1 ) )
        directionalLightNP = self.rootNode.attachNewNode( directionalLight )
        # This light is facing forwards, away from the camera.
        directionalLightNP.setHpr(0, -20, 0)
        self.rootNode.setLight(directionalLightNP)


        #base.saveSphereMap('envmap.jpg', size = 256)

    def setup_events(self):        
        self.button_map = ButtonMap()
        
        self.accept("arrow_left", self.setKey, ["left",1])
        self.accept("arrow_right", self.setKey, ["right",1])
        self.accept("arrow_up", self.setKey, ["up",1])
        self.accept("arrow_down", self.setKey, ["down",1])
        
        self.accept("arrow_left-up", self.setKey, ["left",0])
        self.accept("arrow_right-up", self.setKey, ["right",0])
        self.accept("arrow_up-up", self.setKey, ["up",0])
        self.accept("arrow_down-up", self.setKey, ["down",0])
        
        self.accept("s", self.check_button_press, ['A'])
        self.accept("d", self.check_button_press, ['B'])
        self.accept("a", self.check_button_press, ['C'])
        self.accept("w", self.check_button_press, ['D'])
        self.accept("joy-button", self.check_button_press)
        
        self.accept("music-finished", self.end)
        self.accept("escape", self.end)
    
    def setup_rings(self):
        ring_parsed_info = parse.level_rings(self.info["NAME"], self.difficulty)
        
        #adjust=-0.1
        
        self.ring_list = []
        for pos, beat, button in ring_parsed_info:
            ring = loader.loadModelCopy("./models/ring")
            ring.setName('ring%d'%beat)
            
            tex = loader.loadTexture('./image/envmap.jpg')
            ring.setTexGen(TextureStage.getDefault(), TexGenAttrib.MEyeSphereMap)
            ring.setTexture(tex)
            
            ringY = beat*self.RING_SPACING_PER_BEAT
            self.btn_viewer.append_button(button, beat)
            
            ring.setX(pos[0]*self.FLY_AREA_W)
            ring.setZ(pos[1]*self.FLY_AREA_H)
            
            color = None
            
            if button == 'A':
                color = (.4, .44, .81, 1)
            elif button == 'B':
                color = (1, .3, .3, 1)
            elif button == 'C':
                color = (.99, .0, 1, 1)
            elif button == 'D':
                color = (.39, 1, .62, 1)
            
            ring.setY(ringY)
            ring.reparentTo(self.rootNode)
            ring.setColor(VBase4(*color))

            self.ring_list.append({"node":ring, "time":beat*self.BEAT_DELAY, "button":button, "cleared": False})#+adjust, "button":button, "cleared": False})
        
        ring = self.ring_list[0]["node"]
        
        self.ring_radius = abs(ring.getTightBounds()[0].getZ() -
                               ring.getTightBounds()[1].getZ())/2
                
        self.n_rings = len(self.ring_list)
        
    
    #~ def setup_controller(self, joystick):
        #~ if joystick:
    
    def setKey(self, key, value):
        self.button_map[key] = value
    
    def play(self):
        self.music.play()
        #self.title_msg = gui.TitleMessage(self.info["TITLE"], "by %s" % self.info["ARTIST"])
        
        self.task_list = [name for name in self.__class__.__dict__.keys() if name.startswith("ctask_")]
        
        for task in self.task_list:
            taskMgr.add(getattr(self, task), task)

    def ctask_moveChar(self, task):
        music_time = self.music.getTime()
        
        #~ s_x = 0
        #~ s_z = 0
        
        #~ if self.bunnyActor.getX() > self.FLY_AREA_L:
            #~ if self.button_map["left"] == 1:
                #~ s_x = -1
                
        #~ if self.bunnyActor.getX() < self.FLY_AREA_R:
            #~ if self.button_map["right"] == 1:
                #~ s_x = 1
        
        #~ if self.bunnyActor.getZ() > self.FLY_AREA_B:    
            #~ if self.button_map["down"] == 1:
                #~ s_z = -1*self.y_signal
        
        #~ if self.bunnyActor.getZ() < self.FLY_AREA_T:
            #~ if self.button_map["up"] == 1:
                #~ s_z = 1*self.y_signal
        
        #~ s_x *= self.SPEED_SCALE
        #~ s_z *= self.SPEED_SCALE
        
        if (task.time - self.bunnyActor.last_update) > self.CONTROL_UPDATE_DELAY:
            s_x = self.button_map.get_axis(0)*self.SPEED_SCALE
            s_z = self.button_map.get_axis(1)*self.SPEED_SCALE*self.y_signal
            
            if s_x > 0:
                self.bunnyActor.setR(-15)
                if self.bunnyActor.getCurrentAnim() != "turn-right":
                    self.bunnyActor.loop("turn-right")
            elif s_x < 0:
                self.bunnyActor.setR(15)
                if self.bunnyActor.getCurrentAnim() != "turn-left":
                    self.bunnyActor.loop("turn-left")
            else:
                self.bunnyActor.setR(0)
            
            if s_z > 0:
                self.bunnyActor.setP(-10)
                if self.bunnyActor.getCurrentAnim() != "rise":
                    self.bunnyActor.loop("rise")
            elif s_z < 0:
                self.bunnyActor.setP(+10)
                if self.bunnyActor.getCurrentAnim() != "dive":
                    self.bunnyActor.loop("dive")
            else:
                self.bunnyActor.setP(0)

            if s_z == 0 and s_x == 0:
                if self.bunnyActor.getCurrentAnim() != "fly":
                    self.bunnyActor.loop("fly")
            
            #~ self.bunnyActor.speed.setX(s_x)
            #~ self.bunnyActor.speed.setZ(s_z)
            
            self.bunnyActor.setX(clamp(self.FLY_AREA_L, self.bunnyActor.getX() +  s_x, self.FLY_AREA_R))
            camera.setX(clamp(self.FLY_AREA_L, camera.getX() + s_x, self.FLY_AREA_R))
                
            self.bunnyActor.setZ(clamp(self.FLY_AREA_B, self.bunnyActor.getZ() + s_z, self.FLY_AREA_T))
            camera.setZ(clamp(self.FLY_AREA_B+self.camera_z_offset, camera.getZ() + s_z, self.FLY_AREA_T+self.camera_z_offset))
            
            #~ self.bunnyActor.setPos(self.bunnyActor.getPos() + self.bunnyActor.speed)
            #~ camera.setPos(camera.getPos()+ self.bunnyActor.speed)
            
            self.bunnyActor.last_update = task.time
        
        bunny_pos = time2pos(music_time, self.BEAT_DELAY, self.RING_SPACING_PER_BEAT)
        self.bunnyActor.setY(bunny_pos)
        self.btn_viewer.update(music_time)
        
        self.camera.setY(self.bunnyActor.getY() - self.camera_offset)
        self.skybox.setY(self.bunnyActor.getY())
        
        return Task.cont    
    
    def ctask_terrainPatch(self, task):
        closest_patch = self.terrain_patch_list[0]
        
        if camera.getPos().getY() > closest_patch.getTightBounds()[1].getY():
            last_patch = self.terrain_patch_list[-1]
            closest_patch.setY(last_patch.getPos().getY() + self.terrain_patch_size -0.1)
            
            self.terrain_patch_list.append(self.terrain_patch_list.pop(0))
            
        return Task.cont
    
    def ctask_checkNextRing(self, task):
        pos = self.music.getTime()
        if self.ring_list:
            ring = self.ring_list[0]
            
            if ring["time"] - pos < -0.11:
                self.ring_list.pop(0)
                if not ring["cleared"]:
                    self.miss_sound.play()
                    self.chain = 0
                    self.judgement_stats["MISS"] += 1
                    self.deco_mgr.judgement_msg("MISS", self.chain)
                    #print "MISS (not cleared)", self.chain				
        return Task.cont
    
    def ctask_checkEnd(self, task):
        if self.music.status() == 1:
            messenger.send("music-finished")
        return Task.cont
    
    def end(self):
        #self.title_msg.clear()
        self.rootNode.node().removeAllChildren()
        self.music.stop()
        
        #~ self.p.cleanup()
        #~ self.p.destroy()
        
        for t in self.task_list:
            taskMgr.remove(t)
        
        print "END LEVEL"
        messenger.send("level-finished")
        self.ignoreAll()

    def check_button_press(self, button):        
        time = self.music.getTime()
        if self.ring_list:
            hit = False
            
            next_ring = self.ring_list[0]
            
            if not next_ring["cleared"]:
                time_dist = abs(next_ring["time"] - time)
                
                #ring_dist = self.level.bunnyActor.getDistance(next_ring["node"])
                ring_x = next_ring["node"].getX()
                ring_z = next_ring["node"].getZ()
                
                bunny_x = self.bunnyActor.getX()
                bunny_z = self.bunnyActor.getZ()
                
                ring_dist = math.sqrt((ring_x - bunny_x)**2 + (ring_z - bunny_z)**2)
                
                for s in self.score_list:
                    if time_dist < s:
                        hit = True
                        judgement = self.precision_judge[s]
                        break
                
                if hit:
                    next_ring["cleared"] = True
                    if judgement in ["PERFECT", "GOOD", "OK"]:
                        self.chain += 1
                    elif judgement in ["BAD", "MISS"]:
                        self.chain = 0

                    if ring_dist > self.ring_radius or button != next_ring["button"]:
                        judgement = "MISS"
                        self.chain = 0
                        
                    score = self.score_map[judgement]
                    if score > 0:
                        self.score += score + int(self.chain*0.02*score)
                        self.score_display.update(self.score)
                        
                        #print next_ring["node"]
                        self.btn_viewer.button_hit()
                        #next_ring["node"].removeNode()
                    
                    if judgement == 'MISS':
                        self.miss_sound.play()
                        #self.btn_viewer.button_miss()
                    
                        #~ ring_grow_anim = LerpScaleInterval(next_ring["node"], duration=1.0, startScale=VBase3(1,1,1), scale=VBase3(3,3,3), blendType='easeOut') 
                        #~ ring_fade_anim = LerpFunc(next_ring["node"].setAlphaScale, duration=1.0, fromData=1, toData=0, blendType='easeOut')
                        #~ Parallel(ring_grow_anim, ring_fade_anim).start()
                    
                    self.judgement_stats[judgement] += 1
                    self.deco_mgr.judgement_msg(judgement, self.chain)

class ButtonMap:
    def __init__(self, mode="joy", j_id=0):
        pygame.init()
        pygame.joystick.init()
        
        self.mode = mode
        
        self.last_mouse = [.0, .0]
        self.mouse_sensitivity = 20
        self.mouse_threshold = .01
        if base.mouseWatcherNode.hasMouse():
            self.last_mouse = [base.mouseWatcherNode.getMouseX(), -1*base.mouseWatcherNode.getMouseY()]

        
        try:
            if mode == "joy":
                self.joy = pygame.joystick.Joystick(j_id)
                self.joy.init()
                
                taskMgr.add(self.ctask_JoyEvent, "joy-event")
        except pygame.error, e:
            print e
            self.mode = 'key'
            
        self.buttons = {
            "left":0,
            "right":0,
            "up":0,
            "down":0
        }
            
    def setMode(self, mode):
        self.mode = mode
        
    def __setitem__(self, key, value):
        self.buttons[key] = value
        
    #~ def __getitem__(self, key):
        #~ if self.mode == "key":
            #~ result = self.buttons[key]
        #~ else:
            #~ pygame.event.pump()
            #~ result = {
            #~ "right": 1 if self.joy.get_axis(0) > 0 else 0,
            #~ "left": 1 if self.joy.get_axis(0) < 0 else 0,
            #~ "up": 1 if self.joy.get_axis(1) < 0 else 0,
            #~ "down": 1 if self.joy.get_axis(1) > 0 else 0
            #~ }[key]
            
        #~ return result
        
    def get_axis(self, axis):
        #~ value = .0
        #~ if base.mouseWatcherNode.hasMouse():
            #~ mouse_pos = .0
            #~ if axis == 0:
                #~ mouse_pos = base.mouseWatcherNode.getMouseX()
            #~ elif axis == 1:
                #~ mouse_pos = -1*base.mouseWatcherNode.getMouseY() # Y axis must be inverted here
                
            #~ diff = mouse_pos - self.last_mouse[axis]
            #~ if abs(diff) > self.mouse_threshold:
                #~ #print diff
                #~ value = diff*self.mouse_sensitivity
                
            #~ self.last_mouse[axis] = mouse_pos
            
        #~ return value
        
        if self.mode == "key":
            value = .0
            if axis == 0:
                if self.buttons["left"]:
                    value = -1.0
                elif self.buttons["right"]:
                    value = 1.0
            elif axis == 1:
                if self.buttons["down"]:
                    value = 1.0
                elif self.buttons["up"]:
                    value = -1.0
            
            return value
            
        else:
            return self.joy.get_axis(axis)
        
    def ctask_JoyEvent(self, task):
        pygame.event.pump()
        
        if self.joy.get_button(2): messenger.send("joy-button", ["A"])
        if self.joy.get_button(1): messenger.send("joy-button", ["B"])
        if self.joy.get_button(3): messenger.send("joy-button", ["C"])
        if self.joy.get_button(0): messenger.send("joy-button", ["D"])
            
        return Task.cont
        
if __name__ == '__main__':
    loadPrcFile("./config.prc")
    
    base.enableParticles()
    base.disableMouse()
    base.setBackgroundColor(.0, .0, .0, .0)
    
    level = Level('7stars', difficulty='Normal')
    level.setup()
    level.play()
    
    run()
    
